﻿
Imports System.Data
Imports System.Data.Objects
Imports System.Data.Objects.DataClasses
Imports System.Collections
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Reflection

''' <summary>
''' IBindingList with support for sorting.
''' </summary>
''' <typeparam name="T"></typeparam>
Public Class EntityBindingList(Of T As Class)
    Inherits BindingList(Of T)
    Implements ITypedList
    Implements IBindingListView
    Implements IEntityBindingList

#Region "Fields"

    Private m_DataSource As EntityDataSource
    Private m_ObjectSet As ObjectSet(Of T)
    Private m_EntityCollection As EntityCollection(Of T)
    Private m_Query As IEnumerable
    Private m_Name As String
    Private m_PropertyDescriptor As PropertyDescriptor
    Private m_SortDirection As ListSortDirection
    Private m_DeferNotifications As Boolean
    Private m_AddingNew As Integer

#End Region

#Region "Constructors"

    ''' <summary>
    ''' Initializes a new instance of a <see cref="EntityBindingList"/>.
    ''' </summary>
    ''' <param name="dataSource"><see cref="EntityDataSource"/> that owns the entities on this list.</param>
    ''' <param name="query"><see cref="IEnumerable"/> that provides the entities for this list.</param>
    ''' <param name="name">A name for identifying this list in its parent (for hierarchical binding).</param>
    Public Sub New(ByVal dataSource As EntityDataSource, ByVal query As IEnumerable, ByVal name As String)
        m_DataSource = dataSource
        m_ObjectSet = TryCast(query, ObjectSet(Of T))
        m_EntityCollection = TryCast(query, EntityCollection(Of T))
        m_Query = query
        m_Name = name

        'Load list (at run time only)
        If dataSource IsNot Nothing AndAlso Not dataSource.DesignMode Then
            Refresh()
        Else
            AddNew() ' at design time, add a dummy element to help with binding
        End If
    End Sub

    ''' <summary>
    ''' Initializes a new instance of a <see cref="EntityBindingList"/>.
    ''' </summary>
    ''' <param name="query"><see cref="IEnumerable"/> that provides the entities for this list.</param>
    Public Sub New(ByVal query As IEnumerable)
        Me.New(Nothing, query, String.Empty)
    End Sub

#End Region

#Region "Object model"

    ''' <summary>
    ''' Gets the <see cref="EntityDataSource"/> that owns the elements on this list.
    ''' </summary>
    Public ReadOnly Property DataSource() As EntityDataSource Implements IEntityBindingList.DataSource
        Get
            Return m_DataSource
        End Get
    End Property

    ''' <summary>
    ''' Gets the <see cref="Type"/> of elements on this list.
    ''' </summary>
    Public ReadOnly Property ElementType() As Type Implements IEntityBindingList.ElementType
        Get
            Return GetType(T)
        End Get
    End Property

    ''' <summary>
    ''' Refreshes the list by re-loading all items from the source query.
    ''' </summary>
    Public Sub Refresh() Implements IEntityBindingList.Refresh
        Try
            'Suspend notifications until done
            m_DeferNotifications = True

            'Clear current items
            Clear()

            'Add items in the query
            For Each item As T In EntitySet.GetActiveEntities(m_Query)
                If ApplyFilter(item) Then
                    Me.Add(item)
                End If
            Next

            'Apply sort if any
            If IsSortedCore Then
                ApplySortCore(m_PropertyDescriptor, m_SortDirection)
            End If
        Finally
            'Done
            m_DeferNotifications = False
            OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
        End Try
    End Sub

#End Region

#Region "Overrides"

    ''' <summary>
    ''' Raises the <see cref="ListChanged"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="ListChangedEventArgs"/> that contains the event data.</param>
    Protected Overrides Sub OnListChanged(ByVal e As ListChangedEventArgs)
        If Not m_DeferNotifications Then
            MyBase.OnListChanged(e)

            'Refresh if we are not adding a new item
            If m_AddingNew = 0 Then
                If e.ListChangedType = ListChangedType.ItemChanged OrElse e.ListChangedType = ListChangedType.ItemAdded Then
                    'Refresh list (to honor filters)
                    If m_ObjectSet Is Nothing AndAlso m_Query IsNot Nothing Then
                        Refresh()
                    End If

                    'Refresh sort
                    If e.PropertyDescriptor Is m_PropertyDescriptor Then
                        ApplySortCore(m_PropertyDescriptor, m_SortDirection)
                    End If
                End If
            End If
        End If
    End Sub

    ''' <summary>
    ''' Adds a new item to the end of the collection.
    ''' </summary>
    ''' <returns>The item that was added to the collection.</returns>
    Protected Overrides Function AddNewCore() As Object
        'Create new object
        Dim newObject = TryCast(MyBase.AddNewCore(), T)

        'Add to entity set
        If m_ObjectSet IsNot Nothing Then
            m_ObjectSet.AddObject(newObject)
        ElseIf m_EntityCollection IsNot Nothing Then
            m_EntityCollection.Add(newObject)
        End If

        'And return the new object
        Return newObject
    End Function

    ''' <summary>
    ''' Removes the item at the specified index.
    ''' </summary>
    ''' <param name="index">Index of the item to remove.</param>
    Protected Overrides Sub RemoveItem(index As Integer)
        'Remove from entity set
        If index > -1 Then
            Dim item As T = TryCast(Me(index), T)

            If m_ObjectSet IsNot Nothing Then
                m_ObjectSet.DeleteObject(item)
                'Check that the entity was deleted
                Dim ent = TryCast(item, EntityObject)
                If ent IsNot Nothing Then
                    Debug.Assert(ent.EntityState = EntityState.Deleted OrElse ent.EntityState = EntityState.Detached)
                End If
            ElseIf m_EntityCollection IsNot Nothing Then
                m_EntityCollection.Remove(item)
            End If
        End If

        'And remove from this list
        MyBase.RemoveItem(index)
    End Sub

    ''' <summary>
    ''' Allow adding new only if we have a set.
    ''' </summary>
    Private Overloads ReadOnly Property AllowNew() As Boolean 'Implements IBindingList.AllowNew
        Get
            Return m_ObjectSet IsNot Nothing OrElse m_EntityCollection IsNot Nothing
        End Get
    End Property

    ''' <summary>
    ''' Allow removing only if we have a set.
    ''' </summary>
    Private Overloads ReadOnly Property AllowRemove() As Boolean 'Implements IBindingList.AllowRemove
        Get
            Return m_ObjectSet IsNot Nothing OrElse m_EntityCollection IsNot Nothing
        End Get
    End Property

    Protected Overrides Sub OnAddingNew(ByVal e As AddingNewEventArgs)
        'BindingList<T> weirdness: EndNew is called twice instead of once
        m_AddingNew = 2
        MyBase.OnAddingNew(e)
    End Sub

    Public Overrides Sub CancelNew(ByVal itemIndex As Integer)
        MyBase.CancelNew(itemIndex)
        If itemIndex > -1 Then
            m_AddingNew -= 1
        End If
    End Sub

    Public Overrides Sub EndNew(ByVal itemIndex As Integer)
        MyBase.EndNew(itemIndex)
        If itemIndex > -1 Then
            m_AddingNew -= 1
            If m_AddingNew = 0 AndAlso m_PropertyDescriptor IsNot Nothing Then
                ApplySortCore(m_PropertyDescriptor, m_SortDirection)
            End If
        End If
    End Sub

#End Region

#Region "Sorting support"

    ''' <summary>
    ''' Gets a value indicating whether this list supports sorting.
    ''' </summary>
    Protected Overrides ReadOnly Property SupportsSortingCore() As Boolean
        Get
            Return True
        End Get
    End Property

    ''' <summary>
    ''' Gets the <see cref="PropertyDescriptor"/> that is used for sorting the list.
    ''' </summary>
    Protected Overrides ReadOnly Property SortPropertyCore() As PropertyDescriptor
        Get
            Return m_PropertyDescriptor
        End Get
    End Property

    ''' <summary>
    ''' Gets the <see cref="ListSortDirection"/> that is used for sorting the list.
    ''' </summary>
    Protected Overrides ReadOnly Property SortDirectionCore() As ListSortDirection
        Get
            Return m_SortDirection
        End Get
    End Property

    ''' <summary>
    ''' Gets a value indicating whether the list is currently sorted.
    ''' </summary>
    Protected Overrides ReadOnly Property IsSortedCore() As Boolean
        Get
            Return m_PropertyDescriptor IsNot Nothing
        End Get
    End Property

    ''' <summary>
    ''' Removes any sorting currently applied to the list.
    ''' </summary>
    Protected Overrides Sub RemoveSortCore()
        m_PropertyDescriptor = Nothing
        Refresh()
    End Sub

    ''' <summary>
    ''' Sorts the items on the list.
    ''' </summary>
    ''' <param name="prop"><see cref="PropertyDescriptor"/> that specifies the property to sort on.</param>
    ''' <param name="direction"><see cref="ListSortDirection"/> that specifies whether to sort the list in ascending or descending order.</param>
    Protected Overrides Sub ApplySortCore(ByVal prop As PropertyDescriptor, ByVal direction As ListSortDirection)
        'Get list to sort
        Dim items = TryCast(Me.Items, List(Of T))

        'Apply the sort
        If items IsNot Nothing Then
            'If this property is an EntityObject, get map to sort by display value
            Dim map As ListDictionary = Nothing
            If DataSource IsNot Nothing AndAlso GetType(EntityObject).IsAssignableFrom(prop.PropertyType) Then
                map = DataSource.GetLookupDictionary(prop.PropertyType)
            End If

            'Go sort the list
            Dim pc = New PropertyComparer(Of T)(prop, direction, map)
            items.Sort(pc)
        End If

        'Save new settings and notify listeners
        m_PropertyDescriptor = prop
        m_SortDirection = direction
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
    End Sub

    'PropertyComparer (used to sort the list)
    Private Class PropertyComparer(Of TC)
        Implements IComparer(Of TC)
        Private m_PropertyDescriptor As PropertyDescriptor
        Private m_ListSortDirection As ListSortDirection
        Private m_Map As ListDictionary

        Public Sub New(ByVal pd As PropertyDescriptor, ByVal direction As ListSortDirection, ByVal map As ListDictionary)
            m_PropertyDescriptor = pd
            m_ListSortDirection = direction
            m_Map = map
        End Sub

        Public Function Compare(x As TC, y As TC) As Integer Implements System.Collections.Generic.IComparer(Of TC).Compare
            Try
                'Get values to compare
                Dim o1 As Object = m_PropertyDescriptor.GetValue(x)
                Dim o2 As Object = m_PropertyDescriptor.GetValue(y)

                'Honor mapping (to sort mapped columns by display value)
                If m_Map IsNot Nothing Then
                    ' use mapped values
                    o1 = m_Map(o1.ToString())
                    o2 = m_Map(o2.ToString())
                End If

                'Make sure values are comparable
                Dim v1 = TryCast(o1, IComparable)
                Dim v2 = TryCast(o2, IComparable)

                'Make the comparison
                Dim cmp As Integer = If(v1 Is Nothing AndAlso v2 Is Nothing, 0, If(v1 Is Nothing, +1, If(v2 Is Nothing, -1, v1.CompareTo(v2))))

                'And honor sort direction
                Return If(m_ListSortDirection = ListSortDirection.Ascending, +cmp, -cmp)
            Catch
                Debug.Assert(False, "comparison failed?")
                'Comparison failed...
                Return 0
            End Try
        End Function
    End Class

#End Region

#Region "ITypedList"

    Private Function ITypedList_GetItemProperties(ByVal listAccessors As PropertyDescriptor()) As PropertyDescriptorCollection Implements ITypedList.GetItemProperties
        Dim list = New List(Of PropertyDescriptor)()
        For Each pd As PropertyDescriptor In TypeDescriptor.GetProperties(GetType(T))
            'Skip base class properties (EntityState and EntityKey)
            If pd.ComponentType Is GetType(EntityObject) Then
                Continue For
            End If

            'Add collection properties
            Dim type = pd.PropertyType
            If type.IsGenericType AndAlso type.GetGenericTypeDefinition() Is GetType(EntityCollection(Of )) Then
                list.Add(New CollectionPropertyDescriptor(DataSource, pd))
                Continue For
            End If

            'Add regular properties
            list.Add(pd)
        Next
        Return New PropertyDescriptorCollection(list.ToArray())
    End Function

    Private Function ITypedList_GetListName(ByVal listAccessors As PropertyDescriptor()) As String Implements ITypedList.GetListName
        Return m_Name
    End Function

#End Region

#Region "IBindingListView"

    Private m_Filter As String
    Private m_DataTableFilter As DataTable

    Private Sub ApplySort(ByVal sorts As ListSortDescriptionCollection) Implements IBindingListView.ApplySort
        Throw New NotImplementedException()
    End Sub

    Private Property Filter() As String Implements IBindingListView.Filter
        Get
            Return m_Filter
        End Get
        Set(ByVal value As String)
            m_Filter = value
            UpdateFilterTable()
            Refresh()
        End Set
    End Property

    Private Sub RemoveFilter() Implements IBindingListView.RemoveFilter
        m_Filter = Nothing
        Refresh()
    End Sub

    Private ReadOnly Property SortDescriptions() As ListSortDescriptionCollection Implements IBindingListView.SortDescriptions
        Get
            Return Nothing
        End Get
    End Property

    Private ReadOnly Property SupportsAdvancedSorting() As Boolean Implements IBindingListView.SupportsAdvancedSorting
        Get
            Return False
        End Get
    End Property

    Private ReadOnly Property SupportsFiltering() As Boolean Implements IBindingListView.SupportsFiltering
        Get
            Return True
        End Get
    End Property

    Private Function ApplyFilter(ByVal item As T) As Boolean
        Dim pass As Boolean = True
        If m_DataTableFilter IsNot Nothing Then
            Try
                'Populate the row
                Dim row = m_DataTableFilter.Rows(0)
                For Each pi As PropertyInfo In GetType(T).GetProperties()
                    row(pi.Name) = pi.GetValue(item, Nothing)
                Next

                'Compute the expression
                pass = CBool(row("m_Filter"))
            Catch
            End Try
        End If
        Return pass
    End Function

    Private Sub UpdateFilterTable()
        m_DataTableFilter = Nothing
        If Not String.IsNullOrEmpty(m_Filter) Then
            'Build/rebuild data table
            Dim dt = New DataTable()
            For Each pi As PropertyInfo In GetType(T).GetProperties()
                Dim type = pi.PropertyType
                Dim nt = Nullable.GetUnderlyingType(type)
                If nt IsNot Nothing Then
                    type = nt
                End If
                dt.Columns.Add(pi.Name, type)
            Next

            'Add calculated column that contains the filter
            dt.Columns.Add("m_Filter", GetType(Boolean), m_Filter)

            'Create a single row for evaluating expressions
            If dt.Rows.Count = 0 Then
                dt.Rows.Add(dt.NewRow())
            End If

            'Done, save table
            m_DataTableFilter = dt
        End If
    End Sub

#End Region
End Class

'Property descriptor that converts EntityCollection<T> properties into
'EntityBindingList properties.
Class CollectionPropertyDescriptor
    Inherits PropertyDescriptor
    Private m_EntityDataSource As EntityDataSource
    Private m_PropertyDescriptor As PropertyDescriptor
    Private m_ListType As Type

    Public Sub New(ByVal ds As EntityDataSource, ByVal pd As PropertyDescriptor)
        MyBase.New(pd.Name, Nothing)
        m_EntityDataSource = ds
        m_PropertyDescriptor = pd
        Dim elementType = m_PropertyDescriptor.PropertyType.GetGenericArguments()(0)
        m_ListType = GetType(EntityBindingList(Of ))
        m_ListType = m_ListType.MakeGenericType(elementType)
    End Sub

    Public Overrides ReadOnly Property Name() As String
        Get
            Return m_PropertyDescriptor.Name
        End Get
    End Property

    Public Overrides ReadOnly Property IsReadOnly() As Boolean
        Get
            Return m_PropertyDescriptor.IsReadOnly
        End Get
    End Property

    Public Overrides Sub ResetValue(ByVal component As Object)
        m_PropertyDescriptor.ResetValue(component)
    End Sub

    Public Overrides Function CanResetValue(ByVal component As Object) As Boolean
        Return m_PropertyDescriptor.CanResetValue(component)
    End Function

    Public Overrides Function ShouldSerializeValue(ByVal component As Object) As Boolean
        Return m_PropertyDescriptor.ShouldSerializeValue(component)
    End Function

    Public Overrides ReadOnly Property ComponentType() As Type
        Get
            Return m_PropertyDescriptor.ComponentType
        End Get
    End Property

    Public Overrides ReadOnly Property PropertyType() As Type
        Get
            Return m_ListType
        End Get
    End Property

    Public Overrides Function GetValue(ByVal component As Object) As Object
        Return Activator.CreateInstance(m_ListType, m_EntityDataSource, DirectCast(m_PropertyDescriptor.GetValue(component), IEnumerable), Name)
    End Function

    Public Overrides Sub SetValue(ByVal component As Object, ByVal value As Object)
        m_PropertyDescriptor.SetValue(component, value)
        OnValueChanged(component, EventArgs.Empty)
    End Sub
End Class

''' <summary>
''' Extends the IBindingListView with properties and methods relative to the entity set.
''' </summary>
Public Interface IEntityBindingList
    Inherits IBindingListView
    ''' <summary>
    ''' Gets the <see cref="EntityDataSource"/> that owns the elements on this list.
    ''' </summary>
    ReadOnly Property DataSource() As EntityDataSource
    ''' <summary>
    ''' Gets the type of element on this list.
    ''' </summary>
    ReadOnly Property ElementType() As Type
    ''' <summary>
    ''' Refresh the list by re-loading all items from the source query.
    ''' </summary>
    Sub Refresh()
End Interface